/*
 * block_matrix.c
 */

#ifdef __linux
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/stat.h>
#endif

#include <stdlib.h>
#include <stdio.h>
#include <limits.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <pthread.h>


#include "block_matrix.h"
#include "helper_thread.h"
#include "test_stat.h"


void init_block_matrix(block_matrix_t *mat, int blk_rows, int blk_cols, int elem_rows, int elem_cols) {
	bzero(mat, sizeof(block_matrix_t));
	mat->blk_rows = blk_rows;
	mat->blk_cols = blk_cols;
	mat->blk_size = elem_rows * elem_cols * sizeof(int);
	mat->elem_rows = elem_rows;
	mat->elem_cols = elem_cols;
}

int
open_block_matrix(char *filename, block_matrix_t *mat, int flags, int cache_size) {
	int oflags = (flags & M_CREATE) ? (O_RDWR|O_CREAT) : O_RDWR;
#ifdef __linux
	if (flags & M_DIRECT_IO) {
		oflags |= O_DIRECT;
	}
#endif
	int fd = open(filename, oflags, 0644);

	if (fd > 0) {
		if (flags & M_CREATE) {
			ftruncate(fd, 0);
			lseek(fd, 0, SEEK_SET);
			write(fd, mat, sizeof(block_matrix_t));
		} else {
			lseek(fd, 0, SEEK_SET);
			read(fd, mat, sizeof(block_matrix_t));
		}
		mat->fd = fd;
		mat->flags = flags;
		mat->blk_cache_size = cache_size;
		if (mat->flags & (M_PREFETCH|M_BLK_CACHE)) {
			init_block_cache(&mat->blk_cache, fd, cache_size, mat->blk_size);
		}
#ifdef __APPLE__
		if (mat->flags & M_DIRECT_IO) {
			fcntl(fd, F_NOCACHE, 1);
			fcntl(fd, F_RDAHEAD, 0);
		}
#endif
	}

	return (fd < 0);
}

void
close_block_matrix(block_matrix_t *mat) {
	lseek(mat->fd, 0, SEEK_SET);
	write(mat->fd, mat, sizeof(block_matrix_t));
	close (mat->fd);
	if (mat->flags & (M_PREFETCH|M_BLK_CACHE)) {
		free_block_cache(&mat->blk_cache);
	}
}

void
write_block(block_matrix_t *mat, int *block, int i, int j) {
	test_time_t start = start_timer();
	double time;

	lseek(mat->fd, BLOCK_FD_OFFSET(mat, i, j), SEEK_SET);
	write(mat->fd, block, mat->blk_size);

	time = stop_timer(start, UNIT_USEC);
	ASSIGN_MAX(global_stat.max_write_time, time);
	ASSIGN_MIN(global_stat.min_write_time, time);
	global_stat.write_time += time;
	global_stat.write_cnt++;
}

void
read_block(block_matrix_t *mat, int *block, int i, int j) {
	off_t offset = BLOCK_FD_OFFSET(mat, i, j);
	test_time_t start = start_timer();
	double time;

	int *cached = (mat->flags & (M_PREFETCH|M_BLK_CACHE)) ?
		get_cached_block(&mat->blk_cache, offset) : NULL;

	if (cached) {
		memcpy(block, cached, mat->blk_size);
		if (mat->flags & M_BLK_CACHE) {
			return_cached_block(&mat->blk_cache, offset);
		} else {
			invalidate_cached_block(&mat->blk_cache, offset);
		}
	} else {
		lseek(mat->fd, offset, SEEK_SET);
		read(mat->fd, block, mat->blk_size);
		if (mat->flags & M_BLK_CACHE)
			put_cached_block(&mat->blk_cache, offset, block);
	}

	time = stop_timer(start, UNIT_USEC);
	ASSIGN_MAX(global_stat.max_read_time, time);
	ASSIGN_MIN(global_stat.min_read_time, time);
	global_stat.read_time += time;
	global_stat.read_cnt++;
}

void
add_matrix(block_matrix_t *left, block_matrix_t *right, block_matrix_t *out) {
	int i,j;
	int *block_left = malloc(left->blk_size);
	int *block_right = malloc(left->blk_size);
	int *block_out = malloc(left->blk_size);
	for (i=0;i<left->blk_rows;i++) {
		for (j=0;j<left->blk_cols;j++) {
			WORKER_SYNC();
			read_block(left, block_left, i, j);
			read_block(right, block_right, i, j);
			add_block(out, block_left, block_right, block_out);
			write_block(out, block_out, i, j);
		}
	}
	free(block_left);
	free(block_right);
	free(block_out);
}

void
multiply_matrix(block_matrix_t *left, block_matrix_t *right, block_matrix_t *out) {
	int i,j,k;
	int *block_left = malloc(left->blk_size);
	int *block_right = malloc(left->blk_size);
	int *block_out = malloc(left->blk_size);
	for (i=0;i<left->blk_rows;i++) {
		for (j=0;j<left->blk_cols;j++) {
			bzero(block_out, left->blk_size);
			for (k=0;k<left->blk_rows;k++) { /* We're assuming square matrix */
				WORKER_SYNC();
				read_block(left, block_left, i, k);
				read_block(right, block_right, k, j);
				multiply_block(out, block_left, block_right, block_out);
			}
			write_block(out, block_out, i, j);
		}
	}
	free(block_left);
	free(block_right);
	free(block_out);
}

void
transpose_matrix(block_matrix_t *mat, block_matrix_t *out) {
	int i,j;
	int *block_in = malloc(mat->blk_size);
	int *block_out = malloc(mat->blk_size);
	for (i=0;i<mat->blk_rows;i++) {
		for (j=0;j<mat->blk_cols;j++) {
			read_block(mat, block_in, i, j);
			transpose_block(mat, block_in, block_out);
			write_block(out, block_out, j, i);
		}
	}
	free(block_in);
	free(block_out);
}

void
search_matrix(block_matrix_t *mat, int item, block_matrix_ij_t *result) {
	int i,j,idx;
	int *block = malloc(mat->blk_size);
	for (i=0;i<mat->elem_cols;i++) {
		for (j=0;j<mat->elem_rows;j++) {
			read_block(mat, block, i, j);
			idx = search_block(mat, block, item);
			if (idx >= 0) {
				result->blk_i = i;
				result->blk_j = j;
				result->elem_i = idx / mat->elem_cols;
				result->elem_j = idx % mat->elem_cols;
				return;
			}
		}
	}
	result->blk_i = -1;
	result->blk_j = -1;
	result->elem_i = -1;
	result->elem_j = -1;
}

void
add_block(block_matrix_t *mat, int *left, int *right, int *out) {
	int i,j;
	test_time_t start = start_timer();
	double time;

	for (i=0;i<mat->elem_cols;i++) {
		for (j=0;j<mat->elem_rows;j++) {
			out[BLOCK_IDX(mat,i,j)] = left[BLOCK_IDX(mat,i,j)] + right[BLOCK_IDX(mat,i,j)];
		}
	}

	time = stop_timer(start, UNIT_USEC);
	ASSIGN_MAX(global_stat.max_op_time, time);
	ASSIGN_MIN(global_stat.min_op_time, time);
	global_stat.op_time += time;
	global_stat.op_cnt++;
}

void
multiply_block(block_matrix_t *mat, int *left, int *right, int *out) {
	int i,j,k;
	test_time_t start = start_timer();
	double time;

	for (i=0;i<mat->elem_cols;i++) {
		for (j=0;j<mat->elem_rows;j++) {
			for (k=0;k<mat->elem_cols;k++) { /* Assuming square blocks */
				out[BLOCK_IDX(mat,i,j)] += left[BLOCK_IDX(mat,i,k)] * right[BLOCK_IDX(mat,k,j)];
			}
		}
	}


	time = stop_timer(start, UNIT_USEC);
	ASSIGN_MAX(global_stat.max_op_time, time);
	ASSIGN_MIN(global_stat.min_op_time, time);
	global_stat.op_time += time;
	global_stat.op_cnt++;
}

void
transpose_block(block_matrix_t *mat, int *in, int *out) {
	int i,j;
	test_time_t start = start_timer();
	double time;

	for (i=0;i<mat->elem_rows;i++) {
		for (j=0;j<mat->elem_cols;j++) {
			out[BLOCK_IDX(mat,j,i)] = in[BLOCK_IDX(mat,i,j)];
		}
	}

	time = stop_timer(start, UNIT_USEC);
	ASSIGN_MAX(global_stat.max_op_time, time);
	ASSIGN_MIN(global_stat.min_op_time, time);
	global_stat.op_time += time;
	global_stat.op_cnt++;
}

int
search_block(block_matrix_t *mat, int *in, int item) {
	int i,j;
	test_time_t start = start_timer();
	double time;

	for (i=0;i<mat->elem_rows;i++) {
		for (j=0;j<mat->elem_cols;j++) {
			if(in[BLOCK_IDX(mat,i,j)] == item) {
				return (BLOCK_IDX(mat,i,j));
			}
		}
	}

	time = stop_timer(start, UNIT_USEC);
	ASSIGN_MAX(global_stat.max_op_time, time);
	ASSIGN_MIN(global_stat.min_op_time, time);
	global_stat.op_time += time;
	global_stat.op_cnt++;
	return (-1);
}

void
populate_ordered_matrix(block_matrix_t *mat) {
	int *block = malloc(mat->blk_size);
	int i,j,m,n;
	for (i=0;i<mat->blk_rows;i++) {
		for (j=0;j<mat->blk_cols;j++) {
			for (m=0;m<mat->elem_rows;m++) {
				for (n=0;n<mat->elem_cols;n++) {
					block[BLOCK_IDX(mat,m,n)] = BLOCK_MATRIX_ELEM(mat,i,j,m,n);
				}
			}
			write_block(mat, block, i, j);
		}
	}
	free(block);
}

void
populate_identity_matrix(block_matrix_t *mat) {
	int *block = malloc(mat->blk_size);
	int i,j,m;
	for (i=0;i<mat->blk_rows;i++) {
		for (j=0;j<mat->blk_cols;j++) {
			bzero(block, mat->blk_size);
			if (i==j) {
				for (m=0;m<mat->elem_cols;m++) {
					block[BLOCK_IDX(mat,m,m)] = 1;
				}
			}
			write_block(mat, block, i, j);
		}
	}
}

void
populate_random_matrix(block_matrix_t *mat) {
	int *block = malloc(mat->blk_size);
	int i,j,m, n;
	for (i=0;i<mat->blk_rows;i++) {
		for (j=0;j<mat->blk_cols;j++) {
			for (m=0;m<mat->elem_rows;m++) {
				for (n=0;n<mat->elem_cols;n++) {
					block[BLOCK_IDX(mat,m,n)] = (random() - INT_MAX/2) % 100;
				}
			}
			write_block(mat, block, i, j);
		}
	}
}

void
print_matrix_header(block_matrix_t *mat) {
	printf("fd: %d\n", mat->fd);
	printf("flags: %x\n", mat->flags);
	printf("blk_rows: %d\n", mat->blk_rows);
	printf("blk_cols: %d\n", mat->blk_cols);
	printf("blk_size: %d\n", mat->blk_size);
	printf("elem_rows: %d\n", mat->elem_rows);
	printf("elem_cols: %d\n", mat->elem_cols);
	printf("blk_cache_size %d\n", mat->blk_cache_size);
}

void
print_matrix(block_matrix_t *mat) {
	int i,j,m,n;
	int **block = malloc(mat->blk_cols*sizeof(int *));
	for (j=0;j<mat->blk_cols;j++) {
		block[j] = malloc(mat->blk_size);
	}

	print_matrix_header(mat);

	for (i=0;i<mat->blk_rows;i++) {
		for (j=0;j<mat->blk_cols;j++) {
			read_block(mat,block[j],i,j);
		}
		for (m=0;m<mat->elem_rows;m++) {
			for (j=0;j<mat->blk_cols;j++) {
				for (n=0;n<mat->elem_cols;n++) {
					printf("%8d ", block[j][BLOCK_IDX(mat,m,n)]);
				}
			}
			printf("\n");
		}
	}
	printf("\n");
}


//void
//test(void) {
//	block_matrix_t mat1;
//	block_matrix_t mat2;
//	block_matrix_t mat3;
//	void *ht_args[3];
//	block_matrix_ij_t result;
//	pthread_t ht;
//
//	mat1.elem_cols = 32;
//	mat1.elem_rows = 32;
//	mat1.blk_cols = 64;
//	mat1.blk_rows = 64;
//
//	/* Copy matrix sizes */
//	mat2 = mat1;
//	mat3 = mat1;
//
//	open_block_matrix("test_matrix", &mat1, M_CREATE, 0);
//	populate_ordered_matrix(&mat1);
//	close_block_matrix(&mat1);
//
//	open_block_matrix("test_matrix", &mat1, M_BLK_CACHE|M_DIRECT_IO|M_AIO, 0);
//	open_block_matrix("test2_matrix", &mat2, M_CREATE|M_BLK_CACHE|M_DIRECT_IO|M_AIO, 0);
//	open_block_matrix("test3_matrix", &mat3, M_CREATE, 0);
//
//	populate_identity_matrix(&mat2);
//
//	if (ht_active) {
//		ht_args[0] = &mat1;
//		ht_args[1] = &mat2;
//		ht_args[2] = &mat3;
//		pthread_create(&ht, NULL, multiply_matrix_helper, &ht_args);
//	}
//	multiply_matrix(&mat1, &mat2, &mat3);
//	if (ht_active) pthread_join(ht, NULL);
//
//	search_matrix(&mat1, 70, &result);
//	printf("Search for 70, Resulted Index: (%d,%d,%d,%d)\n", result.blk_i, result.blk_j, result.elem_i, result.elem_j);
//	print_matrix(&mat1);
//	print_matrix(&mat2);
//	print_matrix(&mat3);
//
//	close_block_matrix(&mat1);
//	close_block_matrix(&mat2);
//	close_block_matrix(&mat3);
//}

